#include <stack>
#include "rtarg.h"
#include "texture.h"
#include "logger.h"

using namespace goatgfx;

RenderTarget::RenderTarget()
{
	width = height = 0;
	fbo = 0;
	rbuf_zstencil = 0;
	color_tex = 0;
	tex_face = 0;
	tex_targ = 0;
}

RenderTarget::~RenderTarget()
{
	cleanup();
}

bool RenderTarget::create(unsigned int fmt)
{
	int vp[4];
	glGetIntegerv(GL_VIEWPORT, vp);

	return create(vp[2] - vp[0], vp[3] - vp[1], fmt);
}

bool RenderTarget::create(int width, int height, unsigned int fmt)
{
	debug_log("RenderTarget::create(%d, %d)\n", width, height);
	cleanup();

	tex_targ = GL_TEXTURE_2D;
	this->width = width;
	this->height = height;
	int tex_width = next_pow2(width);
	int tex_height = next_pow2(height);

	CHECKGLERR;
	color_tex = new Texture;
	color_tex->create(tex_width, tex_height, TEX_2D, fmt);
	CHECKGLERR;
	tex_face = 0;

	glGenFramebuffers(1, &fbo);
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);

	glBindTexture(GL_TEXTURE_2D, color_tex->get_id());
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex->get_id(), 0);
	glBindTexture(GL_TEXTURE_2D, 0);

	glGenRenderbuffers(1, &rbuf_zstencil);
	glBindRenderbuffer(GL_RENDERBUFFER, rbuf_zstencil);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, tex_width, tex_height);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbuf_zstencil);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbuf_zstencil);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	CHECKGLERR;
	return true;
}

bool RenderTarget::create(Texture *tex, int face)
{
	debug_log("RenderTarget::create(tex{%d, %d}, face:%d)\n", tex->get_size(0),
			tex->get_size(1), face);

	tex_targ = GL_TEXTURE_2D;
	if(tex->get_type() == TEX_CUBE) {
		if(face >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
			tex_targ = face;
		} else if(face >= 0 && face < 6) {
			tex_targ = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
		} else {
			error_log("invalid face (%d) passed to RenderTarget::create(TextureCube*, int)\n", face);
			return false;
		}
	}

	cleanup();

	width = tex->get_size(0);
	height = tex->get_size(1);

	color_tex = tex;

	glGenFramebuffers(1, &fbo);
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);

	glBindTexture(tex_targ, color_tex->get_id());
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex_targ, color_tex->get_id(), 0);
	glBindTexture(tex_targ, 0);

	glGenRenderbuffers(1, &rbuf_zstencil);
	glBindRenderbuffer(GL_RENDERBUFFER, rbuf_zstencil);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbuf_zstencil);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbuf_zstencil);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	CHECKGLERR;
	return true;
}

void RenderTarget::cleanup()
{
	delete color_tex;
	color_tex = 0;

	if(fbo) {
		glDeleteFramebuffers(1, &fbo);
	}
	if(rbuf_zstencil) {
		glDeleteRenderbuffers(1, &rbuf_zstencil);
	}

	fbo = rbuf_zstencil = 0;
	width = height = 0;
	tex_face = 0;
	tex_targ = 0;
}

bool RenderTarget::resize(int width, int height)
{
	this->width = width;
	this->height = height;
	int tex_width = next_pow2(width);
	int tex_height = next_pow2(height);

	debug_log("resizing render target (fbo %u): %dx%d [%dx%d]\n", fbo, width, height, tex_width, tex_height);

	glBindFramebuffer(GL_FRAMEBUFFER, fbo);

	color_tex->create(tex_width, tex_height, TEX_2D, color_tex->get_format());
	color_tex->bind();
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex_targ, color_tex->get_id(), 0);
	glBindTexture(tex_targ, 0);

	glBindRenderbuffer(GL_RENDERBUFFER, rbuf_zstencil);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, tex_width, tex_height);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	return true;
}

int RenderTarget::get_width() const
{
	return width;
}

int RenderTarget::get_height() const
{
	return height;
}

Texture *RenderTarget::get_texture() const
{
	return color_tex;
}

Matrix4x4 RenderTarget::get_texture_matrix() const
{
	float sx = (float)width / (float)color_tex->get_size(0);
	float sy = (float)height / (float)color_tex->get_size(1);

	// counting on RVO to optimize this away
	return Matrix4x4(sx, 0, 0, 0,
					0, sy, 0, 0,
					0, 0, 1, 0,
					0, 0, 0, 1);
}


static const char *fbstname[] = {
	"GL_FRAMEBUFFER_COMPLETE",
	"GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT",
	"GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT",
	"no such fbo error",
	"GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS",
	"GL_FRAMEBUFFER_INCOMPLETE_FORMATS",
	"GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER",
	"GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER",
	"GL_FRAMEBUFFER_UNSUPPORTED"
};

bool RenderTarget::check() const
{
	bool res = true;

#ifndef GL_ES_VERSION_2_0
	int prev_fb = 0;
	glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prev_fb);
#endif

	glBindFramebuffer(GL_FRAMEBUFFER, fbo);

	int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if(status != GL_FRAMEBUFFER_COMPLETE) {
		error_log("RenderTarget::check: incomplete FBO %u: %s\n", fbo,
				fbstname[status - GL_FRAMEBUFFER_COMPLETE]);
		res = false;
		goto end;
	}

end:
#ifndef GL_ES_VERSION_2_0
	glBindFramebuffer(GL_FRAMEBUFFER, prev_fb);
#endif
	return res;
}

void RenderTarget::bind() const
{
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);

	glViewport(0, 0, width, height);
}

struct Viewport { int vp[4]; };
static std::stack<Viewport> vpstack;

namespace goatgfx {

void set_render_target(const RenderTarget *rtarg)
{
	Viewport vp;

	if(rtarg) {
		glGetIntegerv(GL_VIEWPORT, vp.vp);
		vpstack.push(vp);

		rtarg->bind();
	} else {
#ifdef GL_ES_VERSION_2_0
		extern unsigned int default_fbo;
		glBindFramebuffer(GL_FRAMEBUFFER, default_fbo);
#else
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif

		if(vpstack.empty()) {
			return;
		}
		vp = vpstack.top();
		vpstack.pop();
		glViewport(vp.vp[0], vp.vp[1], vp.vp[2], vp.vp[3]);
	}
}

int next_pow2(int x)
{
	x--;
	x = (x >> 1) | x;
	x = (x >> 2) | x;
	x = (x >> 4) | x;
	x = (x >> 8) | x;
	x = (x >> 16) | x;
	return x + 1;
}

}	// namespace goatgfx
